# __HEADER__

"""XXX"""

import operator
from oldap.filter import Filter

class Reference(object):

    # Filled in by metaclass.
    reference_from = None
    reference_from_attr = None

    """Foreign model reference.

    Usage
    =====
    When defining a model, you can mark that certain attributes refer to other
    models.  You do this by defining reference attributes.  For example::

        class Person(Model):

            base_dn = 'dc=example,dc=com'
            objectClass = ('posixAccount', 'inetOrgPerson')
            rdn_attributes = ('uid',)

        class Group(Model):

            base_dn = 'dc=example,dc=com'
            objectClass = ('posixGroup',)
            rdn_attributes = ('cn',)
            memberUid = Reference(Person, 'uid')

    In this example, the ``Group.memberUid`` attribute refers to the ``uid``
    attribute in the ``Person`` model.  One effect of this is that the
    ``Person`` class now has an attribute that allows it to see all referring
    groups. The attribute is called ``group_reference`` and is a special query
    filter instance that has all the typical Filter methods, plus the following
    methods:

    - ``add(...)``: Each parameter is a model instance to add.
    - ``clear()``: Removes all referring values.
    - ``remove(...)``: Each parameter is a model instance to remove from the
      referrer.
    - ``set(...)``: Replaces all referrer values to the specified set of
      model instances.

    See the `ReferenceFilter` documentation for more detail.

    A sample of interacting with the ``group_reference`` attribute::

        >>> some_person.group_reference
        <oldap.reference.ReferenceFilter object at 0x803c4dd10>
        >>> some_person.group_reference.all()
        [<Group dn='cn=joe,dc=example,dc=com'>,
         <Group dn='cn=admin,dc=example,dc=com'>]
        >>> g = Group(cn=['test'], gidNumber='12345')
        >>> some_person.group_reference.add(g)
        >>> some_person.group_reference.all()
        [<Group dn='cn=joe,dc=example,dc=com'>,
         <Group dn='cn=admin,dc=example,dc=com'>,
         <Group dn='cn=test,dc=example,dc=com'>]

        # set() also works with direct assignment via a descriptor.
        >>> g1 = Group(cn=['test2'], gidNumber='10002')
        >>> g2 = Group(cn=['test3'], gidNumber='10003')
        >>> some_person.group_reference = [g1, g2]
        >>> some_person.group_reference.all()
        [<Group dn='cn=test2,dc=example,dc=com'>,
         <Group dn='cn=test3,dc=example,dc=com'>]

    Triggers
    ========
    You can define triggers which are functions that are called when certain
    events happen on referenced objects.  The different triggers have the following
    signatures:

    - ``on_reference_delete(reference, deleted_object, referring_filter)``

      This is called when a referenced object is deleted.  The parameters are:

        - ``reference``: The `Reference` instance causing the trigger.
        - ``deleted_object``: The model instance being deleted.
        - ``referring_filter``: A query filter of all referring objects.

      This is called before the object is deleted.  You can raise a
      `oldap.exceptions.ReferenceError` exception if you want to abort the
      delete.

    - ``on_reference_update(reference, updated_object, referring_filter,
                            new_attribute_value)``

      This is called when a referenced value is being updated.  The parameters
      are:

        - ``reference``: The `Reference` instance causing the trigger.
        - ``updated_object``: The object being updated.
        - ``referring_filter``: A query filter of all referring objects (to the
          old value).
        - ``old_attribute_value``: The old value for the attribute.
        - ``new_attribute_value``: The new value for the attribute.

      This is called before the attribute is updated on the LDAP server when
      ``save`` is called.  You can raise a `oldap.exceptions.ReferenceError`
      exception if you want to abort the update.

    - ``on_save(reference, updated_object,
                old_attribute_value, new_attribute_value)``

      This is called when a referring value is being created or updated. The
      parameters are:

      - ``reference``: The `Reference` instance causing the trigger.
      - ``updated_object``: The referring object that is being updated.
      - ``old_attribute_value``: The old value for the attribute.
      - ``new_attribute_value``: The new value for the attribute.

      This is called before the attribute is updated on the LDAP server when
      ``save`` is called.  You can raise a `oldap.exceptions.ReferenceError`
      exception if you want to abort the update.

    Reference Delete
    ----------------
    The ``on_reference_delete`` trigger is called when a referenced object is
    being deleted.  For example, if you have a group with ``memberUid`` set to
    ``['user1', 'user2', 'user3']`` and you delete the ``user2`` object, then
    the trigger is called like this::

        on_reference_delete(reference, <Person uid=user2>,
                            <Group filter memberUid contains 'user2'>)

    Reference Update
    ----------------
    The ``on_reference_update`` trigger is called when a referenced object
    attribute is updated.  For example, if you have a group with ``memberUid``
    set to ``['user1', 'user2', 'user3']`` and you modify the ``user2`` uid
    attribute on the Person object to ``['bob']``, then the trigger is called
    like this::

        on_reference_update(reference, <Person uid=user2>,
                            <Group filter memberUid contains 'user2'>,
                            ['user2'], ['bob'])

    Referrer Update
    ---------------
    The ``on_save`` trigger is called when the referring object is created or
    the attribute is updated.  For example, if you have a group with
    ``memberUid`` set to ``['user1', 'user2', 'user3']`` and you append a value
    of ``user4``, then the trigger is called like this::

        on_save(reference, <Group cn='somegroup'>,
                ['user1', 'user2', 'user3'])
                ['user1', 'user2', 'user3', 'user4'])

    Or, if you are creating a new group, it could be called like this::

        on_save(reference, <Group cn='anothergroup'>,
                [],
                ['someuser'])

    It is not called when the object is saved, but the referring attribute is
    not modified.

    :IVariables:
        - `reference_to`: The model class that this is referring to.
        - `reference_to_attr`: The attribute name being referred to.
        - `related_name`: The name on the referenced object for the back
          reference.
        - `reference_from`: The referrer model class.
        - `reference_from_attr`: The attribute name of the referring class.
        - `on_reference_delete`: The on-delete trigger.
        - `on_reference_update`: The on-update trigger.
        - `on_save`: The on-save trigger.

    """

    def __init__(self, reference_to, reference_to_attr,
                 related_name=None,
                 on_reference_delete=None,
                 on_reference_update=None,
                 on_save=None):
        """Foreign reference init.

        See module docstring for details on the trigger functions.

        :Parameters:
            - `reference_to`: The model class that you are referencing.
            - `reference_to_attr`: The attribute name you are referencing.
            - `related_name`: The referenced class will have an attribute that
              provides access to all referring objects.  The name of that
              attribute defaults to ``<class_name>_reference`` where
              ``class_name`` is the lower-case value of the referring class.
              You can change this default by specifying this parameter.  This
              would be necessary if you have multiple attributes pointing to
              the same model.
            - `on_reference_delete`: This is a callable object that is called
              when a referenced object is deleted.
            - `on_reference_update`: This is a callable object that is called
              when the referenced attribute is modified.
            - `on_save`: This is a callable object that is called when
              the referring attribute is created or modified.

        """
        self.reference_to = reference_to
        self.reference_to_attr = reference_to_attr
        self.related_name = related_name
        self.on_reference_delete = on_reference_delete
        self.on_reference_update = on_reference_update
        self.on_save = on_save

    def meta_init(self, reference_from, reference_from_attr):
        self.reference_from = reference_from
        self.reference_from_attr = reference_from_attr
        if self.related_name is None:
            self.related_name = '%s_reference' % (self.reference_from.__name__.lower(),)
        desc = ReferenceDescriptor(self)
        setattr(self.reference_to, self.related_name, desc)

        referrers = self.reference_to._referrers
        if referrers is None:
            referrers = []
            self.reference_to._referrers = referrers
        referrers.append(self)

class ReferenceDescriptor(object):

    def __init__(self, reference):
        self.reference = reference
        self.rf_name = '_rf_%s' % (reference.related_name,)

    def get_rf(self, obj):
        rf = getattr(obj, self.rf_name, None)
        if rf is None:
            value = getattr(obj, self.reference.reference_to_attr)
            if value is None or value == []:
                rf = ReferenceFilter(self.reference, obj)
            else:
                if isinstance(value, list):
                    filters = [Filter(**{self.reference.reference_from_attr: item}) for item in value]
                    filter = reduce(operator.or_, filters)
                    rf = ReferenceFilter(self.reference, obj, filter)
                else:
                    rf = ReferenceFilter(self.reference, obj,
                        **{self.reference.reference_from_attr: value}
                    )
                rf._bind(self.reference.reference_from)
            setattr(obj, self.rf_name, rf)
        return rf

    def __get__(self, obj, objtype):
        if obj is None:
            return self.reference
        else:
            return self.get_rf(obj)

    def __set__(self, obj, value):
        rf = self.get_rf(obj)
        rf.set(value)


class ReferenceFilter(Filter):

    def __init__(self, reference, reference_to_instance, *args, **kwargs):
        self.reference = reference
        self.reference_to_instance = reference_to_instance
        super(ReferenceFilter, self).__init__(*args, **kwargs)

    def add(self, *args):
        # XXX: Support adding string values instance of instances?
        to_value = getattr(self.reference_to_instance,
                           self.reference.reference_to_attr)
        for arg in args:
            assert isinstance(arg, self.reference.reference_from)
            from_value = getattr(arg, self.reference.reference_from_attr)
            if isinstance(to_value, list):
                if len(to_value) > 1:
                    # XXX: Just use the first one?
                    # Or add all of them?
                    raise AssertionError('Referenced value %r has more than one value.' % (self.reference.reference_to_attr,))
                elif len(to_value) == 0:
                    raise AssertionError('Referenced value %r not set.' % (self.reference.reference_to_attr,))
                to_value = to_value[0]
            else:
                if to_value is None:
                    raise AssertionError('Referenced value %r not set.' % (self.reference.reference_to_attr,))
            if isinstance(from_value, list):
                if to_value not in from_value:
                    from_value.append(to_value)
                    arg.save()
            else:
                if from_value != to_value:
                    setattr(arg, self.reference.reference_from_attr, to_value)
                    arg.save()

    def clear(self):
        self._remove(self.all())

    def remove(self, *args):
        self._remove(args)

    def _remove(self, objs):
        to_value = getattr(self.reference_to_instance,
                           self.reference.reference_to_attr)
        for obj in objs:
            assert isinstance(obj, self.reference.reference_from)
            from_value = getattr(obj, self.reference.reference_from_attr)
            if isinstance(from_value, list) and isinstance(to_value, list):
                for to_item in to_value:
                    if to_item in from_value:
                        from_value.remove(to_item)
            elif isinstance(from_value, list):
                # XXX: Is it possible for this to be false?
                if to_value in from_value:
                    from_value.remove(to_value)
            elif isinstance(to_value, list):
                for to_item in to_value:
                    if from_value == to_item:
                        setattr(obj, self.reference.reference_from_attr, None)
            else:
                # Both are not lists.
                if from_value == to_value:
                    setattr(obj, self.reference.reference_from_attr, None)
            obj.save()

    def set(self, *objs):
        # Figure out what to add and remove.
        # This does not handle large data sets very well.
        current = set(self.all())
        new = set(objs)
        to_remove = current - new
        to_add = new - current

        self.add(*to_add)
        self._remove(to_remove)

def on_reference_delete_cascade(reference, deleted_object, referring_filter):
    to_values = getattr(deleted_object, reference.reference_to_attr)
    if isinstance(to_values, list):
        if len(to_values) == 0:
            return
    else:
        # To simplify the code below to make the checks easy.
        if to_values is None:
            return
        else:
            to_values = [to_values]
    for obj in referring_filter.all():
        from_value = getattr(obj, reference.reference_from_attr)
        modified = False
        if isinstance(from_value, list):
            for to_value in to_values:
                if to_value in from_value:
                    from_value.remove(to_value)
                    modified = True
        else:
            for to_value in to_values:
                if to_value == from_value:
                    setattr(obj, reference.reference_from_attr, None)
                    modified = True
        if modified:
            obj.save()

#def on_reference_delete_cascade_delete(reference, deleted_object, referring_filter):

#def on_reference_delete_restrict(reference, deleted_object, referring_filter):
